// 负责编译模板，解析指令和插值表达式
// 负责页面的首次渲染
// 当数据变化后 重新渲染视图
class Compiler {
  constructor(vm) {
    this.vm = vm;
    this.el = vm.$el;
    this.compile(this.el);
  }

  // 编译模板 处理文本节点和元素节点
  compile(el) {
    let childNodes = Array.from(el.childNodes); // 获取到el下所有的节点
    childNodes.forEach(node => {
      if (this.isTextNode(node)) { // 这里判断是否是文本节点
        this.compileText(node);
      } else if (this.isElementNode(node)) {
        this.compileElement(node);
      }
      // 判断node节点是否有子节点 如果有 要递归调用compile
      if (node.childNodes && node.childNodes.length) this.compile(node);
    });
  }

  // 编译元素节点 处理指令
  compileElement(node) {
    // node.attributes 里存了这个节点中所有的属性
    Array.from(node.attributes).forEach(attr => {
      // console.dir(attr); // 每一个attr都是一个对象 name是attr的名字 val是attr的值
      // 比如 v-model='msg' attr.name = v-model attr.value = msg;
      let attrName = attr.name; // 获取name
      if (this.isDirective(attrName)) { // 判断name是否是 v- 开头
        attrName = attrName.substr(2); // 如果是 v- 开头 那么删掉 v-
        let key = attr.value; // 获取到 attr 的value 也就是 vue实例中的某些变量的key
        this.update(node, key, attrName);
      }
    });
  }

  /**
   * 
   * @param {*} node 当前要处理的节点
   * @param {*} key v-xxx 属性绑定的变量的 key
   * @param {*} attrName v-xxx 指令去掉 v- 后的 xxx
   */
  update(node, key, attrName) {
    if (attrName.startsWith('on')) { // 处理 v-on:xxx = "xxx"
      this.eventUpdater(node, attrName.split(':')[1], this.vm.$options.methods[key], key);
      return;
    }
    let updateFn = this[attrName + 'Updater']; // 在当前实例中找到对应的处理函数
    updateFn && updateFn.call(this, node, this.vm[key], key); // 如果找到了就执行它 注意 this
  }

  htmlUpdater(node, value, key) {
    node.innerHTML = value;
    new Watcher(this.vm, key, (newVal) => {
      node.innerHTML = newVal;
    });
  }

  eventUpdater(node, type, fn, fnName) {
    if (!fn) {
      console.error(`There is no function named ${fnName} in Vue methods`);
      return;
    }
    node.addEventListener(type, fn);
  }

  // 如果是 v-text 指令 那么更新当前节点的 textContent
  textUpdater(node, value, key) {
    node.textContent = value;
    new Watcher(this.vm, key, (newVal) => {
      node.textContent = newVal;
    });
  }

  // 如果是 v-model 一般都绑定在表单元素当中 修改当前表单元素的 value 值
  modelUpdater(node, value, key) {
    node.value = value;
    new Watcher(this.vm, key, (newVal) => {
      node.value = newVal;
    });

    // 想v-model绑定的input标签添加一个input事件监听数据的变化
    // input节点的value改变 同时也去改变 Vue 实例下对应变量的值
    // 这个操作会触发 set 中的 notify 通知所有watcher更新视图
    node.addEventListener('input', () => {
      this.vm[key] = node.value;
    });
  }

  // 编译文本节点 处理插值表达式
  compileText(node) {
    let text = node.textContent; // textContent || nodeValue 都可以获取到文本节点的文本内容
    const re = /\{\{(.+)?\}\}/; // 正则匹配 {{ xxx }} 
    if (re.test(text)) { // 如果有匹配到
      const key = RegExp.$1.trim(); // RegExp.$1 可以获取到第一个括号里匹配到的内容
      // 将 匹配到的内容替换成实例 vm 中的变量对应的值
      node.textContent = text.replace(re, this.vm[key]);
      new Watcher(this.vm, key, (newVal) => {
        node.textContent = newVal;
      });
    }
  }

  // 判断元素的属性是否是指令
  isDirective(attrName) {
    return attrName.startsWith('v-');
  }

  // 判断某个节点是否是文本节点 nodeType = 3 文本节点
  isTextNode(node) {
    return node.nodeType === 3;
  }

  // 判断这个节点是否是元素节点 nodeType = 1 元素节点
  isElementNode(node) {
    return node.nodeType === 1;
  }
}